<template>
  <view>
		<view class="chat-input-bar-container">
			<view class="chat-input-bar">
				<view v-if="1===2" class="avatar-item" @click="handleOpen">
					<image class="avatar" src="/static/avatar.png"></image>
				</view>
				<view class="chat-input-container" v-if="!showtextFlag">
					<!-- :adjust-position="false"必须设置，防止键盘弹窗自动上顶，交由z-paging内部处理 -->
					<textarea :focus="focus" style='height: 100px; width:100%;display: flex;justify-content: flex-start;' class="chat-input" v-model="msg" :adjust-position="false" confirm-type="send" type="textarea" placeholder="请输入内容" @confirm="sendClick" />
					<!-- <u--textarea v-model="msg" placeholder="请输入内容" ></u--textarea> -->
					<!-- <u--textarea style='height: 100px;background:pink' :focus="focus" v-model="msg" placeholder="请输入内容" confirm-type="send" type="text" @confirm="sendClick" ></u--textarea> -->
					<view class="changeBox">
						<view class="iconBox">
							<!-- <view class="yuYinBox" @click="recStart" v-if="!startFlag"> -->
                <view class="yuYinBox" @click="shouquanHandle()" v-if="!startFlag">
								<text class="icon iconfont icon-yuyin"></text>
							</view>
            <view class="lineBox"></view>
            <view class="yuYinBox sendBox" @click="sendClick">
              <text class="icon iconfont icon-fasong-shixin"></text>
            </view>
						</view>
					</view>
				</view>
        <view class="videoBox" v-else>
          <view style="display: flex;padding-top:10px">
            <view style="width:10px"></view>
            <view :class="'zxj-record-layer ' + ( statusText==='说话中...' ? ' zxj-recording zxj-recording-animation' : statusText==='松手取消' ? ' zxj-noRecording' : '')">
              <view 
                :class="'zxj-record-btn'" 
                @touchstart="touchstart"
                @touchmove="touchmove"
                @touchend="touchend">
                <text v-if="statusText === '按住说话'">{{ statusText }}</text>
              </view>
              <view style="position:absolute;top: -34px;height: 20px;font-size: 12px;color: #999;" v-if="statusText==='说话中...'" class="cancel-tip">松手发送，上移取消</view>
              <view style="position:absolute;top: -34px;height: 20px;font-size: 12px;" v-if="statusText==='松手取消'" class="cancel-tip">松手取消</view>
              <view class="recwave"  v-if="statusText !== '按住说话'">
                <canvas type="2d" class="recwave-Histogram3"></canvas>
              </view>
              <view class="inputBox" v-if="statusText === '按住说话'" @click.stop="showtextFlag = !showtextFlag">
                <view class="inputBoxInner" >
                  <text class="icon iconfont icon-jianpanshuru1"></text>
                </view>
              </view>
            </view>
          </view>
        </view>
			</view>
		</view>
    <!-- 手撸播放器 -->
		<view style="padding-top:10px;display:none">
			<TestPlayer ref="player" />
		</view>
  </view>
</template>


<script>
/**这里是逻辑层**/
	/**本例子是VUE的选项式 API (Options API)风格，vue2 vue3均支持，如果你使用的vue3 setup组合式 API (Composition API) 编写时，请在import后面取到当前实例this，在需要this的地方传vue3This变量即可，具体调用可以参考 page_vue3____composition_api.vue
	import { getCurrentInstance } from 'vue';
	var vue3This=getCurrentInstance().proxy; //必须定义到最外面，放import后面即可 */
	import TestPlayer from './test_player___.vue'; //手撸的一个跨平台播放器
	/** 先引入Recorder （ 需先 npm install recorder-core ）**/
	import Recorder from 'recorder-core'; //注意如果未引用Recorder变量，可能编译时会被优化删除（如vue3 tree-shaking），请改成 import 'recorder-core'，或随便调用一下 Recorder.a=1 保证强引用
	/** H5、小程序环境中：引入需要的格式编码器、可视化插件，App环境中在renderjs中引入 **/
	// 注意：如果App中需要在逻辑层中调用Recorder的编码/转码功能，需要去掉此条件编译，否则会报未加载编码器的错误
	// #ifdef H5 || MP-WEIXIN
		//按需引入需要的录音格式编码器，用不到的不需要引入，减少程序体积；H5、renderjs中可以把编码器放到static文件夹里面用动态创建script来引入，免得这些文件太大
		import 'recorder-core/src/engine/mp3.js'
		import 'recorder-core/src/engine/mp3-engine.js'
		import 'recorder-core/src/engine/wav.js'
		import 'recorder-core/src/engine/pcm.js'
		import 'recorder-core/src/engine/g711x'
	
		//可选引入可视化插件
		import 'recorder-core/src/extensions/waveview.js'
		import 'recorder-core/src/extensions/wavesurfer.view.js'
	
		import 'recorder-core/src/extensions/frequency.histogram.view.js'
		import 'recorder-core/src/extensions/lib.fft.js'
		
		//实时播放语音，仅支持h5
		import 'recorder-core/src/extensions/buffer_stream.player.js'
		//测试用根据简谱生成一段音乐
		import 'recorder-core/src/extensions/create-audio.nmn2pcm.js'
	// #endif
	
	/** 引入RecordApp **/
	import RecordApp from 'recorder-core/src/app-support/app.js'
	//【所有平台必须引入】uni-app支持文件
	import '../../uni_modules/Recorder-UniCore/app-uni-support.js'
	
	var disableOgg = false;
	// #ifdef MP-WEIXIN
		//可选引入微信小程序支持文件
		import 'recorder-core/src/app-support/app-miniProgram-wx-support.js'
		disableOgg = true; //小程序不测试ogg js文件太大
	// #endif
	
	
	// #ifdef H5 || MP-WEIXIN
		//H5、renderjs中可以把编码器放到static文件夹里面用动态创建script来引入，免得这些文件太大
		import 'recorder-core/src/engine/beta-amr'
		import 'recorder-core/src/engine/beta-amr-engine'
	// #endif
	// #ifdef H5
		//app、h5测试ogg，小程序不测试ogg js文件太大
		import 'recorder-core/src/engine/beta-ogg'
		import 'recorder-core/src/engine/beta-ogg-engine'
	// #endif
	/** 可选：App中引入原生录音插件来进行录音，兼容性和体验更好，原生插件市场地址: https://ext.dcloud.net.cn/plugin?name=Recorder-NativePlugin （试用无任何限制）
		在调用RecordApp.RequestPermission之前进行配置，建议放到import后面直接配置（全局生效）
		也可以判断一下只在iOS上或Android上启用，不判断就都启用，比如判断iOS：RecordApp.UniIsApp()==2 */
	RecordApp.UniNativeUtsPlugin={nativePlugin:true}; //目前仅支持原生插件，uts插件不可用
	//App中提升后台录音的稳定性：配置了原生插件后，可配置 `RecordApp.UniWithoutAppRenderjs=true` 禁用renderjs层音频编码（WebWorker加速），变成逻辑层中直接编码（但会降低逻辑层性能），后台运行时可避免部分手机WebView运行受限的影响
	//App中提升后台录音的稳定性：需要启用后台录音保活服务（iOS不需要），Android 9开始，锁屏或进入后台一段时间后App可能会被禁止访问麦克风导致录音静音、无法录音（renderjs中H5录音也受影响），请调用配套原生插件的`androidNotifyService`接口，或使用第三方保活插件
	
export default {
  components: { TestPlayer },
  data () {
    return {
      startY: 0,
      isRecording: false,
      isShowingCancelTip: false,
      statusText: '按住说话',
      tempFilePath: '',
      msg: '',
      showtextFlag: false,
      
      recType:"mp3",
      recSampleRate:16000,
      recBitRate:16,
      takeoffEncodeChunkSet:false,
      takeoffEncodeChunkMsg:"",
      useAEC:false,
      useANotifySrv:true,
      appUseH5Rec:false,
      showUpload:false,
      recwaveChoiceKey:"Histogram3",
      pageDeep:0,pageNewPath:"main_recTest",
      recpowerx:0,
      disableOgg:disableOgg,
      evalExecCode:"",
      recStart_setSpeaker:false,
      recStart_speakerOff:false,
      recStart_speakerHds:true,
      testNP_PcmPlayerShow:false,
      testMsgs:[],
      reclogs:[],
      reclogLast:""
    }
  },
  mounted () {
    var vueVer=[];
    var vv=typeof(Vue)!="undefined" && Vue && Vue.version; if(vv) vueVer.push("Vue.version:"+vv);
    var v3=(((this.$||{}).appContext||{}).app||{}).version; if(v3) vueVer.push("appContext.app.version:"+v3);
    var v2=(((this.$root||{}).constructor||{}).super||{}).version; if(v2) vueVer.push("constructor.super:"+v2);
    this.reclog("页面mounted("+getCurrentPages().length+"层)"
      +"，Vue="+vueVer.join("/")
      +"，WebViewId="+(this.$root.$page&&this.$root.$page.id||"?")
      +"，ComponentId=_$id:"+(this._$id||"?")+"/"+"$.uid:"+(this.$&&this.$.uid||"?")
      +"，Recorder.LM="+Recorder.LM
      +"，RecordApp.LM="+RecordApp.LM
      +"，UniSupportLM="+RecordApp.UniSupportLM
      +"，UniJsSource="+RecordApp.UniJsSource.IsSource);
    this.pageDeep=getCurrentPages().length;
    this.pageNewPath=/main_recTest/.test(this.getRouteStr())?"page_index2":"main_recTest";
    
    this.isMounted=true;
    this.uniPage__onShow(); //onShow可能比mounted先执行，页面准备好了时再执行一次
    
    //可选，立即显示出环境信息
    this.reclog("正在执行Install，请勿操作...","#f60");
    RecordApp.Install(()=>{
      this.reclog("Install成功，环境："+this.currentKeyTag(),2);
      this.reclog("请先请求录音权限，然后再开始录音");
    },(err)=>{
      this.reclog("RecordApp.Install出错："+err,1);
    });
  },
    /*#ifdef VUE3*/unmounted ()/*#endif*/ /*#ifndef VUE3*/destroyed ()/*#endif*/ {
    RecordApp.Stop() //清理资源，如果打开了录音没有关闭，这里将会进行关闭
  },
  onShow () { //当组件用时没这个回调
    if (this.isMounted) this.uniPage__onShow() //onShow可能比mounted先执行，页面可能还未准备好
    this.recReq()
  },
  methods: {

    // 点击了发送按钮
		sendClick() {
			console.log('this.msg', this.msg)
			if(this.sendState) return false;
			if (!this.msg.length) return false;
			this.$emit('send', this.msg);
		},
		setMsg(msg){
			this.msg = msg;
		},
		handleOpen(){
			this.$emit('handleOpen');
		},
		// 更新了键盘高度（如果不需要切换表情面板则不用写）
		updateKeyboardHeightChange(res) {
			
		},
		// 用户尝试隐藏键盘，此时如果表情面板在展示中，应当隐藏表情面板，如果是键盘在展示中不用处理，z-paging内部已经处理（如果不需要切换表情面板则不用写）
		hidedKeyboard() {
			
		},
    uniPage__onShow () { //页面onShow时【必须调用】的函数，传入当前组件this
      RecordApp.UniPageOnShow(this)
    },



    // 录音 start
    touchstart(e) {
      this.startY = e.touches[0].clientY;
      this.isRecording = true;
      this.statusText = '说话中...';
      const options = {
        duration: 60000,
        sampleRate: 16000,
        numberOfChannels: 1,
        encodeBitRate: 96000,
        format: 'mp3'
      };
      this.recStart()
    },
    touchmove(e) {
      const currentY = e.touches[0].clientY;
      const moveDistance = currentY - this.startY;
      if (this.isRecording) {
        if (moveDistance < -50) {
          this.isShowingCancelTip = true;
          this.statusText = '松手取消';
          // 暂停
          this.recPause()
        } else {
          this.isShowingCancelTip = false;
          this.statusText = '说话中...';
          // 继续
          this.recResume()
        }
      }
    },
    touchend() {
        if (this.isShowingCancelTip) {
          uni.showToast({
            title: '已取消',
            icon: 'none'
          });
          this.quxiaoStop()
        } else {
          this.recStop()
        }
        this.isRecording = false;
        this.isShowingCancelTip = false;
        this.statusText = '按住说话';
    },


    
    uniPage__onShow(){ //页面onShow时【必须调用】的函数，传入当前组件this
      RecordApp.UniPageOnShow(this);
    },
    currentKeyTag(){
      if(!RecordApp.Current) return "[?]";
      // #ifdef APP
      var tag2="Renderjs+H5";
      if(RecordApp.UniNativeUtsPlugin){
        tag2=RecordApp.UniNativeUtsPlugin.nativePlugin?"NativePlugin":"UtsPlugin";
      }
      return RecordApp.Current.Key+"("+tag2+")";
      // #endif
      return RecordApp.Current.Key;
    },
    // 授权
    shouquanHandle(){
      let _this = this
      if(this.appUseH5Rec){//测试时指定使用h5录音
        RecordApp.UniNativeUtsPlugin=null;
      }else{
        RecordApp.UniNativeUtsPlugin={nativePlugin:true}; //恢复原生插件配置值
        RecordApp.UniCheckNativeUtsPluginConfig(); //可以检查一下原生插件配置是否有效
        RecordApp.UniNativeUtsPlugin_JsCall=(data)=>{ //可以绑定原生插件的jsCall回调
          if(data.action=="onLog"){ //显示原生插件日志信息
            this.reclog("[Native.onLog]["+data.tag+"]"+data.message, data.isError?1:"#bbb", {noLog:1});
          }
        };
      }
      RecordApp.UniWebViewActivate(this); //App环境下必须先切换成当前页面WebView
      RecordApp.RequestPermission(()=>{
        if(this.reqOkCall)this.reqOkCall(); this.reqOkCall=null; //留别的组件内调用的回调
      },(msg,isUserNotAllow)=>{
        if(isUserNotAllow){//用户拒绝了录音权限
          //这里你应当编写代码进行引导用户给录音权限，不同平台分别进行编写
          uni.showToast({
            title: msg,
            icon: 'none'
          });
          _this.showtextFlag = false;
          return false
        }
      });
      _this.showtextFlag = true;
      
    },
    recReq(){
      if(this.appUseH5Rec){//测试时指定使用h5录音
        RecordApp.UniNativeUtsPlugin=null;
      }else{
        RecordApp.UniNativeUtsPlugin={nativePlugin:true}; //恢复原生插件配置值
        RecordApp.UniCheckNativeUtsPluginConfig(); //可以检查一下原生插件配置是否有效
        RecordApp.UniNativeUtsPlugin_JsCall=(data)=>{ //可以绑定原生插件的jsCall回调
          if(data.action=="onLog"){ //显示原生插件日志信息
            this.reclog("[Native.onLog]["+data.tag+"]"+data.message, data.isError?1:"#bbb", {noLog:1});
          }
        };
      }
      RecordApp.UniWebViewActivate(this); //App环境下必须先切换成当前页面WebView
      RecordApp.RequestPermission(()=>{
        if(this.reqOkCall)this.reqOkCall(); this.reqOkCall=null; //留别的组件内调用的回调
      },(msg,isUserNotAllow)=>{
        if(isUserNotAllow){//用户拒绝了录音权限
          //这里你应当编写代码进行引导用户给录音权限，不同平台分别进行编写
        }
      });
    },
    recStart(){
      this.$refs.player.setPlayBytes(null);
      this.takeoffEncodeChunkMsg="";var takeEcCount=0,takeEcSize=0;
      this.takeEcChunks=this.takeoffEncodeChunkSet?[]:null;
      this.watchDogTimer=0; this.wdtPauseT=0; var processTime=0;
      
      if(this.useANotifySrv) this.tryStart_androidNotifyService(); //Android App+原生插件环境下，如需后台或锁屏录音，就必须启用后台录音保活服务（iOS不需要），Android 9开始，锁屏或进入后台一段时间后App可能会被禁止访问麦克风导致录音静音、无法录音（renderjs中H5录音、原生插件录音均受影响），因此需要调用原生插件的`androidNotifyService`接口保活，或使用第三方保活插件
      
      this.reclog(this.currentKeyTag()+" 正在打开...");
      RecordApp.UniWebViewActivate(this); //App环境下必须先切换成当前页面WebView
      RecordApp.Start({
        type:this.recType
        ,sampleRate:this.recSampleRate
        ,bitRate:this.recBitRate
        ,audioTrackSet:!this.useAEC?null:{ //配置回声消除，H5、App、小程序均可用，但并不一定会生效；注意：H5、App+renderjs中需要在请求录音权限前进行相同配置RecordApp.RequestPermission_H5OpenSet后此配置才会生效
          noiseSuppression:true,echoCancellation:true,autoGainControl:true
        }
        
        ,setSpeakerOff:!this.recStart_setSpeaker? null : { //使用原生录音插件时，可以提供一个扬声器外放和听筒播放的切换默认配置
          off:this.recStart_speakerOff, headset:this.recStart_speakerHds
        }
        
        ,onProcess:(buffers,powerLevel,duration,sampleRate,newBufferIdx,asyncEnd)=>{
          //全平台通用：可实时上传（发送）数据，配合Recorder.SampleData方法，将buffers中的新数据连续的转换成pcm上传，或使用mock方法将新数据连续的转码成其他格式上传，可以参考Recorder文档里面的：Demo片段列表 -> 实时转码并上传-通用版；基于本功能可以做到：实时转发数据、实时保存数据、实时语音识别（ASR）等
          
          //注意：App里面是在renderjs中进行实际的音频格式编码操作，此处的buffers数据是renderjs实时转发过来的，修改此处的buffers数据不会改变renderjs中buffers，所以不会改变生成的音频文件，可在onProcess_renderjs中进行修改操作就没有此问题了；如需清理buffers内存，此处和onProcess_renderjs中均需要进行清理，H5、小程序中无此限制
          //注意：如果你要用只支持在浏览器中使用的Recorder扩展插件，App里面请在renderjs中引入此扩展插件，然后在onProcess_renderjs中调用这个插件；H5可直接在这里进行调用，小程序不支持这类插件；如果调用插件的逻辑比较复杂，建议封装成js文件，这样逻辑层、renderjs中直接import，不需要重复编写
          
          this.recpowerx=powerLevel;
          this.recpowert=this.formatTime(duration,1)+" / "+powerLevel;
          processTime=Date.now();
          
          //H5、小程序等可视化图形绘制，直接运行在逻辑层；App里面需要在onProcess_renderjs中进行这些操作
          // #ifdef H5 || MP-WEIXIN
          var wave=this.waveStore&&this.waveStore[this.recwaveChoiceKey];
          if(wave){
            wave.input(buffers[buffers.length-1],powerLevel,sampleRate);
          }
          // #endif
          //实时语音通话对讲，实时处理录音数据
          if(this.wsVoiceProcess) this.wsVoiceProcess(buffers,powerLevel,duration,sampleRate,newBufferIdx);
          
          //实时释放清理内存，用于支持长时间录音；在指定了有效的type时，编码器内部可能还会有其他缓冲，必须同时提供takeoffEncodeChunk才能清理内存，否则type需要提供unknown格式来阻止编码器内部缓冲，App的onProcess_renderjs中需要进行相同操作
          if(this.takeEcChunks){
            if(this.clearBufferIdx>newBufferIdx){ this.clearBufferIdx=0 } //重新录音了就重置
            for(var i=this.clearBufferIdx||0;i<newBufferIdx;i++) buffers[i]=null;
            this.clearBufferIdx=newBufferIdx;
          }
        }
        ,onProcess_renderjs:`function(buffers,powerLevel,duration,sampleRate,newBufferIdx,asyncEnd){
          //App中在这里修改buffers才会改变生成的音频文件
          //App中是在renderjs中进行的可视化图形绘制，因此需要写在这里，this是renderjs模块的this（也可以用This变量）；如果代码比较复杂，请直接在renderjs的methods里面放个方法xxxFunc，这里直接使用this.xxxFunc(args)进行调用
          var wave=this.waveStore&&this.waveStore[this.recwaveChoiceKey];
          if(wave) wave.input(buffers[buffers.length-1],powerLevel,sampleRate);
          
          //和onProcess中一样进行释放清理内存，用于支持长时间录音
          if(${this.takeEcChunks?1:0}){
            if(this.clearBufferIdx>newBufferIdx){ this.clearBufferIdx=0 } //重新录音了就重置
            for(var i=this.clearBufferIdx||0;i<newBufferIdx;i++) buffers[i]=null;
            this.clearBufferIdx=newBufferIdx;
          }
        }`
        
        ,takeoffEncodeChunk:!this.takeoffEncodeChunkSet?null:(chunkBytes)=>{
          //全平台通用：实时接收到编码器编码出来的音频片段数据，chunkBytes是Uint8Array二进制数据，可以实时上传（发送）出去
          //App中如果未配置RecordApp.UniWithoutAppRenderjs时，建议提供此回调，因为录音结束后会将整个录音文件从renderjs传回逻辑层，由于uni-app的逻辑层和renderjs层数据交互性能实在太拉跨了，大点的文件传输会比较慢，提供此回调后可避免Stop时产生超大数据回传
          takeEcCount++; takeEcSize+=chunkBytes.byteLength;
          this.takeoffEncodeChunkMsg="已接收到"+takeEcCount+"块，共"+takeEcSize+"字节";
          this.takeEcChunks.push(chunkBytes);
          
          //App中使用原生插件时，可方便的将数据实时保存到同一文件，第一帧时append:false新建文件，后面的append:true追加到文件
          //RecordApp.UniNativeUtsPluginCallAsync("writeFile",{path:"xxx.mp3",append:回调次数!=1, dataBase64:RecordApp.UniBtoa(chunkBytes.buffer)}).then(...).catch(...)
        }
        ,takeoffEncodeChunk_renderjs:!this.takeoffEncodeChunkSet?null:`function(chunkBytes){
          //App中这里可以做一些仅在renderjs中才生效的事情，不提供也行，this是renderjs模块的this（也可以用This变量）
        }`
        
        ,start_renderjs:`function(){
          //App中可以放一个函数，在Start成功时renderjs中会先调用这里的代码，this是renderjs模块的this（也可以用This变量）
          //放一些仅在renderjs中才生效的事情，比如初始化，不提供也行
        }`
        ,stop_renderjs:`function(aBuf,duration,mime){
          //App中可以放一个函数，在Stop成功时renderjs中会先调用这里的代码，this是renderjs模块的this（也可以用This变量）
          this.audioData=aBuf; //留着给Stop时进行转码成wav播放
        }`
      },()=>{
        this.reclog(this.currentKeyTag()+" 录制中："+this.recType
          +" "+this.recSampleRate+" "+this.recBitRate+"kbps"
          +(this.takeoffEncodeChunkSet?" takeoffEncodeChunk":"")
          +(this.useAEC?" useAEC":"")
          +(this.appUseH5Rec?" appUseH5Rec":""),2);
        //创建音频可视化图形绘制
        this.initWaveStore();
        
        //【稳如老狗WDT】可选的，监控是否在正常录音有onProcess回调，如果长时间没有回调就代表录音不正常
        if(RecordApp.Current.CanProcess()){
          var wdt=this.watchDogTimer=setInterval(()=>{
            if(wdt!=this.watchDogTimer){ clearInterval(wdt); return } //sync
            if(Date.now()<this.wdtPauseT) return; //如果暂停录音了就不检测：puase时赋值this.wdtPauseT=Date.now()*2（永不监控），resume时赋值this.wdtPauseT=Date.now()+1000（1秒后再监控）
            if(Date.now()-(processTime||startTime)>1500){ clearInterval(wdt);
              this.reclog(processTime?"录音被中断":"录音未能正常开始",1);
              // ... 错误处理，关闭录音，提醒用户
            }
          },1000);
        }else{
          this.reclog("当前环境不支持onProcess回调，不启用watchDogTimer","#aaa"); //目前都支持回调
        }
        var startTime=Date.now();
      },(msg)=>{
        this.reclog(this.currentKeyTag()+" 开始录音失败："+msg,1);
      });
    },
    recPause(){
      if(RecordApp.GetCurrentRecOrNull()){
        RecordApp.Pause();
        this.wdtPauseT=Date.now()*2; //永不监控onProcess超时
        this.reclog("已暂停");
      }
    },
    recResume(){
      if(RecordApp.GetCurrentRecOrNull()){
        RecordApp.Resume();
        this.wdtPauseT=Date.now()+1000; //1秒后再监控onProcess超时
        this.reclog("继续录音中...");
      }
    },
    recStopX(){
      this.tryClose_androidNotifyService(); //关闭后台录音保活服务
      this.watchDogTimer=0; //停止监控onProcess超时
      RecordApp.Stop(
        null //success传null就只会清理资源，不会进行转码
        ,(msg)=>{
          this.reclog("已清理，错误信息："+msg);
        }
      );
    },
    arrayBufferToBase64 (arrayBuffer) {
      // 创建一个 Uint8Array 视图来读取 ArrayBuffer
      const uint8Array = new Uint8Array(arrayBuffer)

      // 将 Uint8Array 转换为二进制字符串
      let binaryString = ''
      for (let i = 0; i < uint8Array.length; i++) {
        binaryString += String.fromCharCode(uint8Array[i])
      }

      // 使用 btoa() 函数将二进制字符串转换为 Base64 编码
      return btoa(binaryString)
    },
    recStop(){
      const _this = this;
      this.reclog("正在结束录音...");
      this.tryClose_androidNotifyService(); //关闭后台录音保活服务
      this.watchDogTimer=0; //停止监控onProcess超时
      uni.showToast({
        title: '转译中',
        icon: 'loading'
      });
      RecordApp.Stop((aBuf,duration,mime)=>{
        //全平台通用：aBuf是ArrayBuffer音频文件二进制数据，可以保存成文件或者发送给服务器
        //App中如果在Start参数中提供了stop_renderjs，renderjs中的函数会比这个函数先执行
        const base64String = this.arrayBufferToBase64(aBuf)
        uni.request({
          url: '/yuyin-admin/yuyin/getText', // 替换为实际的 API 地址
          method: 'post',
          data: {
            file: base64String,
          },
          header: {
            'Content-Type': 'application/x-www-form-urlencoded'
          },
          success: function (res) {
            const _msg = _this.msg
            // _this.msg += res.data.text
            _this.msg = res.data.text
            uni.hideLoading();
            if(!res.data.text){
              uni.showToast({
                title: '未识别到语音',
                icon: 'none'
              });
              setTimeout(() => {
                uni.hideLoading();
              }, 1000);
            }else{
              _this.sendClick()
              setTimeout(() => {
                _this.msg = _msg
              }, 500);
            }
          },
          fail: function (err) {
            console.error('请求失败:', err)
            uni.hideLoading();
          },
          complete: function () {
            console.log('请求完成')
            uni.hideLoading();
          }
        })

        var recSet=(RecordApp.GetCurrentRecOrNull()||{set:{type:this.recType}}).set;
        var testPlay_aBuf_renderjs="this.audioData";
        
        //如果使用了takeoffEncodeChunk，Stop的aBuf长度是0，数据早已存到了takeEcChunks数组里面，直接合并成完整音频文件
        if(this.takeEcChunks){
          testPlay_aBuf_renderjs=""; //renderjs的数据是空的
          this.reclog("启用takeoffEncodeChunk后Stop返回的blob长度为0不提供音频数据");
          var len=0; for(var i=0;i<this.takeEcChunks.length;i++)len+=this.takeEcChunks[i].length;
          var chunkData=new Uint8Array(len);
          for(var i=0,idx=0;i<this.takeEcChunks.length;i++){
            var itm=this.takeEcChunks[i]; chunkData.set(itm,idx); idx+=itm.length;
          }
          aBuf=chunkData.buffer;
          this.reclog("takeoffEncodeChunk接收到的音频片段，已合并成一个音频文件 "+aBuf.byteLength+"字节");
        }
        
        //【测试】用变量保存起来，别的地方调用
        this.lastRecType=recSet.type;
        this.lastRecBuffer=aBuf;
        
        //【测试】播放，部分格式会转码成wav播放
        this.$refs.player.setPlayBytes(aBuf,testPlay_aBuf_renderjs,duration,mime,recSet,Recorder);
      },(msg)=>{
        this.reclog("结束录音失败："+msg,1);
      });
    },
    quxiaoStop(){
      this.reclog("正在结束录音...");
      this.tryClose_androidNotifyService(); //关闭后台录音保活服务
      this.watchDogTimer=0; //停止监控onProcess超时
      RecordApp.Stop((aBuf,duration,mime)=>{
        //全平台通用：aBuf是ArrayBuffer音频文件二进制数据，可以保存成文件或者发送给服务器
        //App中如果在Start参数中提供了stop_renderjs，renderjs中的函数会比这个函数先执行
        var recSet=(RecordApp.GetCurrentRecOrNull()||{set:{type:this.recType}}).set;
        var testPlay_aBuf_renderjs="this.audioData";
        
        //如果使用了takeoffEncodeChunk，Stop的aBuf长度是0，数据早已存到了takeEcChunks数组里面，直接合并成完整音频文件
        if(this.takeEcChunks){
          testPlay_aBuf_renderjs=""; //renderjs的数据是空的
          this.reclog("启用takeoffEncodeChunk后Stop返回的blob长度为0不提供音频数据");
          var len=0; for(var i=0;i<this.takeEcChunks.length;i++)len+=this.takeEcChunks[i].length;
          var chunkData=new Uint8Array(len);
          for(var i=0,idx=0;i<this.takeEcChunks.length;i++){
            var itm=this.takeEcChunks[i]; chunkData.set(itm,idx); idx+=itm.length;
          }
          aBuf=chunkData.buffer;
          this.reclog("takeoffEncodeChunk接收到的音频片段，已合并成一个音频文件 "+aBuf.byteLength+"字节");
        }
        
        //【测试】用变量保存起来，别的地方调用
        this.lastRecType=recSet.type;
        this.lastRecBuffer=aBuf;
        
        //【测试】播放，部分格式会转码成wav播放
        this.$refs.player.setPlayBytes(aBuf,testPlay_aBuf_renderjs,duration,mime,recSet,Recorder);
      },(msg)=>{
        this.reclog("结束录音失败："+msg,1);
      });
    },
    
    
    //Android App启用后台录音保活服务，需要原生插件支持，注意必须RecordApp.RequestPermission得到权限后调用
    tryStart_androidNotifyService(){
      if(RecordApp.UniIsApp() && !this._tips_anfs){ this._tips_anfs=1;
        this.reclog("App中提升后台录音的稳定性：需要启用后台录音保活服务（iOS不需要），Android 9开始，锁屏或进入后台一段时间后App可能会被禁止访问麦克风导致录音静音、无法录音（App中H5录音也受影响），需要原生层提供搭配常驻通知的Android后台录音保活服务（Foreground services）；可调用配套原生插件的androidNotifyService接口，或使用第三方保活插件","#4face6");
      }
      if(RecordApp.UniIsApp()!=1) return; //非Android App不处理
      if(!RecordApp.UniNativeUtsPlugin) return; //未使用原生插件
      
      this._NotifyService=false;
      RecordApp.UniNativeUtsPluginCallAsync("androidNotifyService",{
        title:"正在录音"
        ,content:"正在录音中，请勿关闭App运行"
      }).then((data)=>{
        this._NotifyService=true;
        var nCode=data.notifyPermissionCode, nMsg=data.notifyPermissionMsg;
        this.reclog("搭配常驻通知的Android后台录音保活服务已打开，ForegroundService已运行(通知可能不显示或会延迟显示，并不影响服务运行)，通知显示状态(1有通知权限 3可能无权限)code="+nCode+" msg="+nMsg,2);
      }).catch((e)=>{
        this.reclog("原生插件的androidNotifyService接口调用出错："+e.message,1);
        this.reclog("如果你已集成了配套的原生录音插件，并且是打包自定义基座运行，请检查本项目根目录的AndroidManifest.xml里面是否已经解开了注释，否则被注释掉的service不会包含在App中",1);
      });
    },
    tryClose_androidNotifyService(){
      if(!this._NotifyService) return;   this._NotifyService=false;
      RecordApp.UniNativeUtsPluginCallAsync("androidNotifyService",{
        close:true
      }).then(()=>{
        this.reclog("已关闭搭配常驻通知的Android后台录音保活服务");
      }).catch((e)=>{
        this.reclog("原生插件的androidNotifyService接口调用出错："+e.message,1);
      });
    },
    
    
    clearLogs(){ 
      this.testMsgs=[];
      this.reclogs=[]; 
    },
    addTestMsg(msg,color){
      var now=new Date();
      var t=("0"+now.getHours()).substr(-2)
        +":"+("0"+now.getMinutes()).substr(-2)
        +":"+("0"+now.getSeconds()).substr(-2);
      var txt="["+t+"]"+msg;
      this.testMsgs.splice(0,0,{msg:txt,color:color});
      this.reclogLast={txt:txt,color:color};
    },
    reclog(msg,color,set){
      var now=new Date();
      var t=("0"+now.getHours()).substr(-2)
        +":"+("0"+now.getMinutes()).substr(-2)
        +":"+("0"+now.getSeconds()).substr(-2);
      var txt="["+t+"]"+msg;
      if(!set||!set.noLog)console.log(txt);
      this.reclogs.splice(0,0,{txt:txt,color:color});
      this.reclogLast={txt:txt,color:color};
    },
    recTypeClick(e){
      var type=e.target.dataset.type;
      if(type){
        this.recType=type;
      }
    },
    appUseH5RecClick(){
      this.appUseH5Rec=!this.appUseH5Rec;
      RecordApp.Current=null;
      this.reclog('切换了appUseH5Rec='+this.appUseH5Rec+'，重新请求录音权限后生效',"#f60");
    },
    
    // 可视化波形，这里是一次性创建多个波形，可以参考page_i18n.vue只创建一个波形会简单一点
    initWaveStore(){
      //if(isWx && this.waveStore)return;//可以限制初始化一次；如果你的canvas所在的view存在v-if之类的会重新创建了对应的view，必须将波形重新进行初始化才能使用；如果使用的是同一个view，重新初始化后如果上次的动画没有完成时，小程序中开头部分新的波形会和老的波形相互干扰，老动画完成后恢复正常，App、H5无此问题
      var store=this.waveStore=this.waveStore||{};//这个测试demo会创建多个可视化，所以用个对象存起来，只有一个的时候请直接用个变量存一下即可 不用搞这么复杂
      var webStore=`var store=this.waveStore=this.waveStore||{};`;//在renderjs中执行，this是renderjs模块的this
      webStore+=`this.recwaveChoiceKey="${this.recwaveChoiceKey}";`;//把当前选中的波形也传过去
      RecordApp.UniFindCanvas(this,[".recwave-Histogram3"],`${webStore}
        store.Histogram3=Recorder.FrequencyHistogramView({compatibleCanvas:canvas1, width:100%, height:300
          ,lineCount:20,position:0,minHeight:10,fallDuration:400,stripeEnable:false,mirrorEnable:true
          ,linear:[0,"rgb(43, 102, 246)",1,"rgb(43, 102, 246)"]});
      `,(canvas1)=>{
        store.Histogram3=Recorder.FrequencyHistogramView({compatibleCanvas:canvas1, width:300, height:300
          ,lineCount:20,position:0,minHeight:10,fallDuration:400,stripeEnable:false,mirrorEnable:true
          ,linear:[0,"rgb(43, 102, 246)",1,"rgb(43, 102, 246)"]});
      });
    },
    formatTime(ms,showSS){
      var ss=ms%1000;ms=(ms-ss)/1000;
      var s=ms%60;ms=(ms-s)/60;
      var m=ms%60;ms=(ms-m)/60;
      var h=ms, v="";
      if(h>0) v+=(h<10?"0":"")+h+":";
      v+=(m<10?"0":"")+m+":";
      v+=(s<10?"0":"")+s;
      if(showSS)v+="″"+("00"+ss).substr(-3);;
      return v;
    },
    getRouteStr(){
      var url=this.$page&&this.$page.route||this.$root.route; //vue2 || vue3
      if(!url && this.$root.$scope){//wx
        url=this.$root.$scope.route;
      }
      return "/"+url;
    },
    reloadPage(){
      var url=this.getRouteStr();
      console.log("刷新页面 url="+url);
      if(getCurrentPages().length==1){
        uni.reLaunch({ url:url })
      }else{
        uni.navigateBack({animationDuration:0,success:()=>{ setTimeout(()=>{
          uni.navigateTo({url:url})
        },300); }});
      }
    }
    // 录音 end
		
  }
}
</script>

<style lang='scss'>
	.chat-input-bar {
		display: flex;
		flex-direction: row;
		align-items: center;
		border-top: solid 1px #f5f5f5;
		/* background-color: #f8f8f8; */
		
		padding: 5px 10px;
	}
	.chat-input-container {
		flex: 1;
		width: 100%;
		/* #ifndef APP-NVUE */
		display: flex;
		/* #endif */
		padding: 7px;
		/* background-color: white; */
		border-radius: 10px;
		flex-direction: column;
		border: 1px solid rgba(0,0,0,0.15);
		.changeBox{
			height: 30px;
			width: 100%;
			margin-top: 10px;
			display: flex;
			justify-content: flex-end;
			.iconBox{
				display: flex;
				align-items: center;
				// width: 200px;
			}
			.yuYinBox{
				height: 30px;
				width: 30px;
				display: flex;
				align-items: center;
				justify-content: center;
				border-radius: 100%;
				border: 2px solid #1E1E1E;
				// margin-right: 5px;
				.icon-yuyin{
					font-size: 16px;
				}
			}
			.lineBox{
				height: 80%;
				background: rgba(0,0,0,0.15);
				width: 2px;
				margin: 0 8px;
			}
			.sendBox{
				// margin-left: 5px;
				background: green;
				border: none;
				.icon-fasong-shixin{
					color: #fff;
					font-size: 16px;
				}
			}
			.viewBox{
				height: 33px;
				width: 199px;
				background: #ccd6e9;
				border-radius: 4px;
			}
		}
	}
  .videoBox{
    height: 50px;
    width: 100%;
    border-radius: 12px;
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    .textBox{
      color: #1f1f1f;
      letter-spacing: 2px;
      font-size: 14px;
      font-weight: 500;
      width: 100%;
      height: 100%;
      display: flex;
      align-items: center;
      justify-content: center;
      border-radius: 4px;
      position: absolute;
      z-index: 10;

    }
    .inputBox{
      height: 100%;
      width: 70px;
      display: flex;
      align-items: center;
      justify-content: flex-end;
      position: absolute;
      right: 0px;
      top: 0px;
      z-index: 10000;
      padding: 10px 6px 10px 10px;
      .inputBoxInner{
        height: 30px;
        width: 30px;
        border: 2px solid #1E1E1E;
        border-radius: 100%;
        display: flex;
        align-items: center;
        justify-content: center;
      }
      .iconfont{
        font-weight: 700;
        font-size: 16px;
      }
    }
  }
	.chat-input {
		// flex: 1;
		font-size: 14px;
	}
	::deep .uni-input-wrapper{
		justify-content: flex-start !important;
	}
	.avatar{
		width: 27px;
		height: 27px;
		border-radius: 27px;
		margin-right: 8px;
	}

	.chat-input-send {
		background-color: #007AFF;
		margin: 5px 5px 5px 10px;
		border-radius: 5px;
		width: 55px;
		height: 30px;
		/* #ifndef APP-NVUE */
		display: flex;
		/* #endif */
		justify-content: center;
		align-items: center;
	}
	.chat-input-send-text {
		color: white;
		font-size: 13px;
	}
	.chat-input-send-disabled{
		background-color: #656262;
		opacity: 0.4;
	}
</style>

<style>
  .recwaveChoice{
    cursor: pointer;
    display:inline-block;
    vertical-align: bottom;
    border-right:1px solid #ccc;
    background:#ddd;
    line-height:28px;
    font-size:12px;
    color:#666;
    padding:0 5px;
  }
  .recwaveChoice:first-child{
    border-radius: 99px 0 0 99px;
  }
  .recwaveChoice:last-child{
    border-radius: 0 99px 99px 0;
    border-right:none;
  }
  .recwaveChoice.slc,.recwaveChoice:hover{
    background:#f60;
    color:#fff;
  }
  .recwave{
    height:50px;
    width:100%;
    position: absolute;
    bottom: 0;
    z-index: 1;
  }
  .recwave canvas{
    margin: 0 auto;
    height:50px;
    width:300px;
  }
  </style>
  <style lang="scss">
  .zxj-record-layer {
    width: 100%;
    box-sizing: border-box;
    height: 50px;
    z-index: 10;
    display: flex;
    flex-direction: column;
    align-items: center;
    background: #fff;
    // border: 1px solid red;
    box-shadow: 0px 0px 6px 0px rgba(0,0,0,0.15);
    border-radius: 12px;
    font-weight: 500;
    letter-spacing: 2px;
    position: absolute;
    left: 0;
    top: 0;
  }

  .zxj-record-btn {
    font-size: 16px;
    line-height: 50px;
    width: 100%;
    height: 50px;
    text-align: center;
    color: #000;
    position: absolute;
    left: 0;
    top: 0;
    z-index: 100;
  }

  .cancel-tip {
    color: red;
    margin-top: 10px;
  }
  .zxj-recording{
    color: #fff;
    background: rgba(43, 102, 246, 0.1);
    border: 1px solid rgb(43, 102, 246);
    box-shadow: 0px 0px 6px 0px rgba(43, 102, 246,0.15);
  }
  .zxj-noRecording{
    color: #fff;
    background:  rgba(234, 51, 35, 0.1);
    box-shadow: 0px 0px 6px 0px rgba(234, 51, 35,0.15);
    border: 1px solid rgb(234, 51, 35);
  }
  .zxj-recording-animation {
    // animation: recordingFlash 1s infinite;
  }
  @keyframes recordingFlash {
    0% {
      opacity: 1;
    }
    50% {
      opacity: 0.5;
    }
    100% {
      opacity: 1;
    }
  }
  </style>